package streaming;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.Vector;

import simulation.Config;
import simulation.DistributionAction;
import simulation.Distributions;
import simulation.PeriodicInsertion;
import simulation.PeriodicRemoval;

public class Overlay {

	private static Overlay instance;
	private Vector<Vector<Integer>> rings; 
	
	PeriodicInsertion periodicInsertion;
	PeriodicRemoval periodicRemoval;
	public Distributions insertionDistribution;
	public Distributions removalDistribution;
	
	HashMap hosts, removedHosts;
	int numHosts;
	Integer lastIdInserted; // id of the last host inserted. Used to hosts inserted by periodic event, to use a proper id 

	// temporary
	ArrayList ring[];
//	ArrayList sourceNeighbors;
	int countNonByz;
	
	private Overlay() {
		this.hosts = new HashMap();
		this.removedHosts = new HashMap();
		this.rings = new Vector<Vector<Integer>>();
		
		/* Checks whether simulation will have dynamic insertion/removal of hosts
		 * ATTENTION: If the user selects to use dynamic insertion using distributions, the static insertion - the one with fixes values -
		 * will be ignored.
		 */
		if (StreamingConfig.DYNAMIC_INSERTION_ON){
			this.periodicInsertion = new PeriodicInsertion(this);

			if (StreamingConfig.USE_INSERTION_DISTRIBUTION){
				insertionDistribution = new Distributions(DistributionAction.INSERTION, StreamingConfig.INSERTION_DISTRIBUTION_TYPE);
				this.generateProbabilisticInsertionEvents();
			}
			else {
				// begins host insertion event if using a fixed value instead of probabilistic distribution
				this.periodicInsertion.addEvent();
			}
		}
		
		if (StreamingConfig.DYNAMIC_REMOVAL_ON) {
			this.periodicRemoval = new PeriodicRemoval(this);
			if (StreamingConfig.USE_REMOVAL_DISTRIBUTION){
				removalDistribution = new Distributions(DistributionAction.REMOVAL, StreamingConfig.REMOVAL_DISTRIBUTION_TYPE);
				this.generateProbabilisticRemovalEvents();
			}
			else {
				// begins host removal event if using a fixed value instead of probabilistic distribution
				this.periodicRemoval.addEvent();
			}
		}
		
		this.lastIdInserted = new Integer(-1); // -1 .:. none has been created
	}

	public static void resetOverlay() {
		instance = null;
	}
	
	/**
	 * Returns overlay's instance. If it doesn't exist, create.
	 * @return An instance of Overlay
	 * @author emanuel
	 */
	public static Overlay getInstance() {
		if (instance == null){
			instance = new Overlay();
		}
		return instance;
	}

	/**
	 * @brief This method generates the events of hosts insertion on system, based on data from configuration files.
	 */
	private void generateProbabilisticInsertionEvents(){
		int values[] = new int[(int)Config.TIME];
		
		for (int i = 0 ; i < StreamingConfig.HOSTS_NUMBER_TO_INSERT_ON_DISTRIBUTION; i++){
			int value = insertionDistribution.getNextInt();
			
			if (value > 0 && value < (int)Config.TIME){
				values[value]++;
				this.periodicInsertion.addEvent(value);
			}
		}
		
		// Save values on a file to build graphic with insertions
		PrintStream out;
		
		String filename = "probabilisticInsertionValues.csv";
		
		try {
			out = new PrintStream(new FileOutputStream(filename));
			
			for (int i = 0 ; i < (int) Config.TIME; i++){
				out.print(i + "," + values[i]);
				out.println();
			}
			out.flush();
			out.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * @brief This method generates the events of hosts removal on system, based on data from configuration files.
	 */
	private void generateProbabilisticRemovalEvents(){
		int values[] = new int[(int)Config.TIME];
		
		for (int i = 0 ; i < StreamingConfig.HOSTS_NUMBER_TO_REMOVE_ON_DISTRIBUTION; i++){
			int value = removalDistribution.getNextInt();
			
			if (value > 0 && value < (int)Config.TIME){
				values[value]++;
				this.periodicRemoval.addEvent(value);
			}
		}
		
		// Save values on a file to build graphic with insertions
		PrintStream out;
		
		String filename = "probabilisticRemovalValues.csv";
		
		try {
			out = new PrintStream(new FileOutputStream(filename));
			
			for (int i = 0 ; i < (int) Config.TIME; i++){
				out.print(i + "," + values[i]);
				out.println();
			}
			out.flush();
			out.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	public void addHost(Host host) {
		hosts.put(new Integer(host.id), host);
		
		if (host.id > this.lastIdInserted){
			this.lastIdInserted = host.id;
		}
		
		// SourceHost doesn't count as host
		if (host.getId() != 0)	
			this.setNumHosts(this.getNumHosts() + 1);
	}

	/**
	 * Creates a new host and adds it to the topology 
	 * @author emanuel
	 */
	public void addHostByEvent(){
		Random rnd = new Random();
		
		// adds a new event only if the insertion isn't by probabilistic distribution
		if (!StreamingConfig.USE_INSERTION_DISTRIBUTION){		
			this.periodicInsertion.addEvent();
		}
		
		this.lastIdInserted++;
		
		Host host;
		if (StreamingConfig.SIMULATION_WITH_ATTACKERS){
			// needs to decide if the host will be normal or attacker
			double dblRnd = rnd.nextDouble();
			
			// if the double generated is lower than the attackers ratio, then, the new host to be created is an attacker
			// otherwise, its a normal host
			if (dblRnd <= StreamingConfig.ATTACKERS_RATIO){
				// Creates new attacker host
				switch (StreamingConfig.ATTACK_TYPE) {
					case 1:
						host = new Attacker1(this.lastIdInserted);
						break;
					case 2:
						host = new Attacker2(this.lastIdInserted);
						break;
					case 3:
						host = new Attacker3(this.lastIdInserted);
						break;
					case 4:
						host = new Attacker4(this.lastIdInserted);
						break;
					case 5:
						host = new Attacker5_Polluter(this.lastIdInserted);
						break;
					default:
						host = new Host(this.lastIdInserted);
						break;
				}
			}
			else {
				host = new Host(this.lastIdInserted);
			}
		}
		else {
			host = new Host(this.lastIdInserted);
		}
		
		hosts.put(new Integer(host.id), host);
		
		this.setNumHosts(this.getNumHosts() + 1);
		
		for (int i = 0 ; i < rings.size(); i++){
			// as j will be used as the index where the new host will be inserted,
			// the +1 is to give the chance to insert in the last position of array,
			// being neighbor of the index-0 host and index-j-1 Host
			
			int j = rnd.nextInt(this.getNumHosts()); 
			int idLeftNeighbor = -1;
			int idRightNeighbor = -1;
			
			this.getRings().get(i).add(j, this.lastIdInserted);
			
			// if 0 was selected as the position on the ring i to insert new Host, left
			// neighbor is the one in the last position of array
			if (j == 0){
				idLeftNeighbor = this.getRings().get(i).get(this.getNumHosts() - 1);
				idRightNeighbor = this.getRings().get(i).get(j+1);
			}
			// if j is in the last position in array (number of hosts - 1), then the right neighbor
			// is the one on position 0
			else if (j == (this.getNumHosts() - 1)){
				idLeftNeighbor = this.getRings().get(i).get(j-1);
				idRightNeighbor = this.getRings().get(i).get(0);
			}
			else {
				idLeftNeighbor = this.getRings().get(i).get(j-1);
				idRightNeighbor = this.getRings().get(i).get(j+1);
			}
			
			// Gets j-1 and j+1 (position of host that was shifted right) Hosts, once j position holds new Host id
			updateNeighbors(i,
					idLeftNeighbor,
					idRightNeighbor,
					this.lastIdInserted);
		}
	}
	
	/**
	 * After the creation of a new host, the neighbors must be updated, inserting this new node
	 * and adding as neighbor of other hosts
	 * @param ring In which ring is this update happening
	 * @param idLeftNeighbor Id of the left neighbor 
	 * @param idRightNeighbor id of the right neighbor
	 * @param idNewNeighbor Id of the new host
	 * @author emanuel
	 */
	public void updateNeighbors(int ring, int idLeftNeighbor, int idRightNeighbor, int idNewNeighbor){
		Host leftHost = this.getHost(idLeftNeighbor);
		Host rightHost = this.getHost(idRightNeighbor);
		Host newHost = this.getHost(idNewNeighbor);
		
		// Remove old neighbor
		leftHost.removeNeighbor(ring, idRightNeighbor);
		rightHost.removeNeighbor(ring, idLeftNeighbor);

		// Add new neighbor
		leftHost.addNeighbor(ring, idNewNeighbor);
		rightHost.addNeighbor(ring, idNewNeighbor);
		
		// Add neighbors to new host
		newHost.addNeighbor(ring, idLeftNeighbor);
		newHost.addNeighbor(ring, idRightNeighbor);
	}
	
	/**
	 * Removes a Host from system 
	 * @author emanuel
	 */
	public void removeHostByEvent(){
		// adds a new event only if the insertion isn't by probabilistic distribution
		if (!StreamingConfig.USE_REMOVAL_DISTRIBUTION){
			this.periodicRemoval.addEvent();
		}
		
		Random rnd = new Random();
		
		// This simulator doesn't consider that Source will fail. So, remove source from list hosts to choose one to be removed
		SourceHost source = (SourceHost) hosts.remove(0);
		Object[] hostsList = hosts.values().toArray();
		
		Host hostToBeRemoved = (Host) hostsList[rnd.nextInt(hostsList.length)];
//		System.out.println(hostToBeRemoved.getId());
		// Put source again on the list of hosts
		hosts.put(0, source);
		
		for (int i = 0 ; i < rings.size(); i++){
			int hostPosition = -1;
			int idLeftNeighbor = -1;
			int idRightNeighbor = -1;
			
			for (int j = 0 ; j < this.getRings().get(i).size() ; j++){
				// If the value is equal, found the position of the 'host' in the ring
				if (this.getRings().get(i).get(j).equals(hostToBeRemoved.getId())){
					hostPosition = j;
					break;
				}
			}
			
			if (hostPosition == -1){
				System.out.println("Error. Could not find host in ring " + i + ".");
				System.exit(1);
			}
			
			// if the position in the ring of host to be removed is 0 left neighbor is in the last position of array
			if (hostPosition == 0){
				idLeftNeighbor = this.getRings().get(i).get(this.getNumHosts() - 1);
				idRightNeighbor = this.getRings().get(i).get(hostPosition+1);
			}
			// if the host to be removed is in the last position in array (number of hosts - 1), then the right neighbor
			// is the one on position 0
			else if (hostPosition == (this.getNumHosts() - 1)){
				idLeftNeighbor = this.getRings().get(i).get(hostPosition-1);
				idRightNeighbor = this.getRings().get(i).get(0);
			}
			else {
				idLeftNeighbor = this.getRings().get(i).get(hostPosition-1);
				idRightNeighbor = this.getRings().get(i).get(hostPosition+1);
			}
			
			// Gets j-1 and j+1 (position of host that was shifted right) Hosts, once j position holds new Host id
			removeHostFromRing(i, hostToBeRemoved, idLeftNeighbor,	idRightNeighbor);
			this.getRings().get(i).remove(hostPosition);
		}

		// Remove host from sources neighbors (if needed)
		for (Host host : source.getNeighbors()){
			if (host.getId().equals(hostToBeRemoved.getId())){
				source.removeNeighbor(hostToBeRemoved.getId());
				
				// If remove a neighbor from the source, selects a new one
				Host neighbor = (Host) hostsList[rnd.nextInt(hostsList.length)];
				source.addNeighbor(neighbor.id);
				neighbor.addNeighbor(0);
				
				break;
			}
		}

		// After remove host from rings, must remove from hosts list
		this.removeHost(hostToBeRemoved.getId());
		
		this.setNumHosts(this.getNumHosts() - 1);
	}
	
	/**
	 * Remove a Host from ring
	 * @param ring The ring number
	 * @param hostToBeRemoved To that will be removed from ring
	 * @param idLeftNeighbor Id of the neighbor in the left
	 * @param idRightNeighbor Id of the neighbor in the right
	 * 
	 */
	private void removeHostFromRing(int ring, Host hostToBeRemoved, int idLeftNeighbor, int idRightNeighbor) {
		Host leftHost = this.getHost(idLeftNeighbor);
		Host rightHost = this.getHost(idRightNeighbor);
		
		// Remove host from both neighbors
		leftHost.removeNeighbor(ring, hostToBeRemoved.getId());
		rightHost.removeNeighbor(ring, hostToBeRemoved.getId());
	}

	public void removeHost(int id) {
		removedHosts.put(new Integer(id), (Host)hosts.get(new Integer(id)));
		hosts.remove(new Integer(id));
		//System.out.println("Host removido pelo churn: " + id);
	}

	public Host getHost(int id) {
		return (Host) hosts.get(new Integer(id));
	}

	public Host[] getHosts() {
		if (hosts.size() == 0)
			return null;
		return (Host[]) hosts.values().toArray(new Host[hosts.size()]);
	}
	
	public Host[] getAllHosts() {
		HashMap allHosts = new HashMap(hosts);
		allHosts.putAll(removedHosts);
		
		if (allHosts.size() == 0)
			return null;
		return (Host[]) allHosts.values().toArray(new Host[allHosts.size()]);
	}

	public void setRings(Vector<Vector<Integer>> rings) {
		this.rings = rings;
	}

	public Vector<Vector<Integer>> getRings() {
		return rings;
	}
	
	public void setNumHosts(int numHosts) {
		this.numHosts = numHosts;
	}

	private int getNumHosts(){
		return this.numHosts;
	}
	
	public double getLatency(Host h1, Host h2) {
		// Finish this!!
		return 0.05;
		// Vector p = getShortestPathTable(v1.getStubVertex(),
		// v2.getStubVertex()) ;
		// double latency =
		// (p==null)?2*Config.LAN_LATENCY:(getLatency(p)+2*Config.LAN_LATENCY) ;
		// return (latency) ;
	}
	
	public Distributions getInsertionDistribution() {
		return insertionDistribution;
	}

	public void setInsertionDistribution(Distributions insertionDistribution) {
		this.insertionDistribution = insertionDistribution;
	}

	public Distributions getRemovalDistribution() {
		return removalDistribution;
	}

	public void setRemovalDistribution(Distributions removalDistribution) {
		this.removalDistribution = removalDistribution;
	}
}